home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / ui.py < prev   
Encoding:
Python Source  |  2009-04-06  |  63.9 KB  |  1,625 lines

  1. '''Abstract Apport user interface.
  2.  
  3. This encapsulates the workflow and common code for any user interface
  4. implementation (like GTK, Qt, or CLI).
  5.  
  6. Copyright (C) 2007 Canonical Ltd.
  7. Author: Martin Pitt <martin.pitt@ubuntu.com>
  8.  
  9. This program is free software; you can redistribute it and/or modify it
  10. under the terms of the GNU General Public License as published by the
  11. Free Software Foundation; either version 2 of the License, or (at your
  12. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  13. the full text of the license.
  14. '''
  15.  
  16. import glob, sys, os.path, optparse, time, traceback, locale, gettext, re
  17. import pwd, errno, urllib, zlib
  18. import subprocess, threading, webbrowser
  19. from gettext import gettext as _
  20.  
  21. import apport, apport.fileutils, REThread
  22.  
  23. from apport.crashdb import get_crashdb
  24.  
  25. def thread_collect_info(report, reportfile, package):
  26.     '''Encapsulate call to add_*_info() and update given report,
  27.     so that this function is suitable for threading.
  28.  
  29.     If reportfile is not None, the file is written back with the new data.'''
  30.  
  31.     report.add_gdb_info()
  32.     if not package:
  33.         if report.has_key('ExecutablePath'):
  34.             package = apport.fileutils.find_file_package(report['ExecutablePath'])
  35.         else:
  36.             raise KeyError, 'called without a package, and report does not have ExecutablePath'
  37.     report.add_package_info(package)
  38.     report.add_os_info()
  39.     report.add_hooks_info()
  40.  
  41.     # add title
  42.     title = report.standard_title()
  43.     if title:
  44.         report['Title'] = title
  45.  
  46.     # check package origin
  47.     if 'Package' not in report or \
  48.         not apport.packaging.is_distro_package(report['Package'].split()[0]):
  49.         if 'APPORT_REPORT_THIRDPARTY' in os.environ or \
  50.             apport.fileutils.get_config('main', 'thirdparty', False, bool=True):
  51.             report['ThirdParty'] = 'True'
  52.         else:
  53.             #TRANS: %s is the name of the operating system
  54.             report['UnreportableReason'] = _('This is not a genuine %s package') % \
  55.                 report['DistroRelease'].split()[0]
  56.  
  57.     # check obsolete packages
  58.     if report['ProblemType'] == 'Crash' and \
  59.         'APPORT_IGNORE_OBSOLETE_PACKAGES' not in os.environ:
  60.         old_pkgs = report.obsolete_packages()
  61.         if old_pkgs:
  62.             report['UnreportableReason'] = _('You have some obsolete package \
  63. versions installed. Please upgrade the following packages and check if the \
  64. problem still occurs:\n\n%s') % ', '.join(old_pkgs)
  65.  
  66.     report.anonymize()
  67.  
  68.     if reportfile:
  69.         f = open(reportfile, 'a')
  70.         os.chmod (reportfile, 0)
  71.         report.write(f, only_new=True)
  72.         f.close()
  73.         apport.fileutils.mark_report_seen(reportfile)
  74.         os.chmod (reportfile, 0600)
  75.  
  76. class UserInterface:
  77.     '''Abstract base class for encapsulating the workflow and common code for
  78.        any user interface implementation (like GTK, Qt, or CLI).
  79.  
  80.        A concrete subclass must implement all the abstract ui_* methods.'''
  81.  
  82.     def __init__(self):
  83.         '''Initialize program state and parse command line options.'''
  84.  
  85.         self.gettext_domain = 'apport'
  86.         self.report = None
  87.         self.report_file = None
  88.         self.cur_package = None
  89.  
  90.         try:
  91.             self.crashdb = get_crashdb(None)
  92.         except ImportError, e:
  93.             # this can happen while upgrading python packages
  94.             print >> sys.stderr, 'Could not import module, is a package upgrade in progress? Error:', e
  95.             sys.exit(1)
  96.  
  97.         gettext.textdomain(self.gettext_domain)
  98.         self.parse_argv()
  99.  
  100.     #
  101.     # main entry points
  102.     #
  103.  
  104.     def run_crashes(self):
  105.         '''Present all currently pending crash reports to the user, ask him
  106.         what to do about them, and offer to file bugs for them.
  107.         
  108.         Return True if at least one crash report was processed, False
  109.         otherwise.'''
  110.  
  111.         result = False
  112.  
  113.         for f in apport.fileutils.get_new_reports():
  114.             self.run_crash(f)
  115.             result = True
  116.  
  117.         return result
  118.  
  119.     def run_crash(self, report_file, confirm=True):
  120.         '''Present given crash report to the user, ask him what to do about it,
  121.         and offer to file a bug for it.
  122.         
  123.         If confirm is False, the user will not be asked whether to report the
  124.         problem.'''
  125.  
  126.         self.report_file = report_file
  127.  
  128.         try:
  129.             try:
  130.                 apport.fileutils.mark_report_seen(report_file)
  131.             except OSError:
  132.                 # not there any more? no problem, then it won't be regarded as
  133.                 # "seen" any more anyway
  134.                 pass
  135.             if not self.load_report(report_file):
  136.                 return
  137.  
  138.             if 'Ignore' in self.report:
  139.                 return
  140.  
  141.             # check for absent CoreDumps (removed if they exceed size limit)
  142.             if self.report.get('ProblemType') == 'Crash' and \
  143.                 'Signal' in self.report and 'CoreDump' not in self.report and \
  144.                 'Stacktrace' not in self.report:
  145.                 subject = os.path.basename(self.report.get('ExecutablePath',
  146.                     _('unknown program')))
  147.                 heading = _('Sorry, the program "%s" closed unexpectedly') % subject
  148.                 self.ui_error_message(_('Problem in %s') % subject,
  149.                     '%s\n\n%s' % (heading, _('Your computer does not have enough \
  150. free memory to automatically analyze the problem and send a report to the developers.')))
  151.                 return
  152.  
  153.             # check unsupportable flag
  154.             if self.report.has_key('UnsupportableReason'):
  155.                 self.ui_info_message(_('Unreportable problem'),
  156.                     _('The current configuration cannot be supported:\n\n%s') %
  157.                     self.report['UnsupportableReason'])
  158.                 return
  159.  
  160.             # ask the user about what to do with the current crash
  161.             if not confirm:
  162.                 pass
  163.             elif self.report.get('ProblemType') == 'Package':
  164.                 response = self.ui_present_package_error()
  165.                 if response == 'cancel':
  166.                     return
  167.                 assert response == 'report'
  168.             elif self.report.get('ProblemType') == 'KernelCrash':
  169.                 response = self.ui_present_kernel_error()
  170.                 if response == 'cancel':
  171.                     return
  172.                 assert response == 'report'
  173.             elif self.report.get('ProblemType') == 'KernelOops':
  174.                 # XXX the string doesn't quite match this case
  175.                 response = self.ui_present_kernel_error()
  176.                 if response == 'cancel':
  177.                     return
  178.                 assert response == 'report'
  179.             else:
  180.                 try:
  181.                     desktop_entry = self.get_desktop_entry()
  182.                 except ValueError: # package does not exist
  183.                     self.ui_error_message(_('Invalid problem report'),
  184.                         _('The report belongs to a package that is not installed.'))
  185.                     self.ui_shutdown()
  186.                     return
  187.  
  188.                 response = self.ui_present_crash(desktop_entry)
  189.                 assert response.has_key('action')
  190.                 assert response.has_key('blacklist')
  191.  
  192.                 if response['blacklist']:
  193.                     self.report.mark_ignore()
  194.  
  195.                 if response['action'] == 'cancel':
  196.                     return
  197.                 if response['action'] == 'restart':
  198.                     self.restart()
  199.                     return
  200.                 assert response['action'] == 'report'
  201.  
  202.             # we want to file a bug now
  203.             try:
  204.                 self.collect_info()
  205.             except (IOError, zlib.error):
  206.                 # can happen with broken core dumps
  207.                 self.report = None
  208.                 self.ui_error_message(_('Invalid problem report'),
  209.                     _('This problem report is damaged and cannot be processed.'))
  210.                 return False
  211.             except ValueError: # package does not exist
  212.                 self.ui_error_message(_('Invalid problem report'),
  213.                     _('The report belongs to a package that is not installed.'))
  214.                 self.ui_shutdown()
  215.                 return
  216.  
  217.             # check unreportable flag
  218.             if self.report.has_key('UnreportableReason'):
  219.                 self.ui_info_message(_('Problem in %s') % self.report['Package'].split()[0],
  220.                     _('The problem cannot be reported:\n\n%s') %
  221.                     self.report['UnreportableReason'])
  222.                 return
  223.  
  224.             if self.handle_duplicate():
  225.                 return
  226.  
  227.             if self.report.get('ProblemType') in ['Crash', 'KernelCrash',
  228.                                                   'KernelOops']:
  229.                 response = self.ui_present_report_details()
  230.                 if response == 'cancel':
  231.                     return
  232.                 if response == 'reduced':
  233.                     try:
  234.                         del self.report['CoreDump']
  235.                     except KeyError:
  236.                         pass # Huh? Should not happen, but did in https://launchpad.net/bugs/86007
  237.                 else:
  238.                     assert response == 'full'
  239.  
  240.             self.file_report()
  241.         except IOError, e:
  242.             # fail gracefully if file is not readable for us
  243.             if e.errno in (errno.EPERM, errno.EACCES):
  244.                 self.ui_error_message(_('Invalid problem report'),
  245.                     _('You are not allowed to access this problem report.'))
  246.                 sys.exit(1)
  247.             elif e.errno == errno.ENOSPC:
  248.                 self.ui_error_message(_('Error'),
  249.                     _('There is not enough disk space available to process this report.'))
  250.                 sys.exit(1)
  251.             else:
  252.                 self.ui_error_message(_('Invalid problem report'), e.strerror)
  253.                 sys.exit(1)
  254.         except OSError, e:
  255.             # fail gracefully on ENOMEM
  256.             if e.errno == errno.ENOMEM:
  257.                 print >> sys.stderr, 'Out of memory, aborting'
  258.                 sys.exit(1)
  259.             else:
  260.                 raise
  261.  
  262.     def run_report_bug(self):
  263.         '''Report a bug.
  264.  
  265.         If a pid is given on the command line, the report will contain runtime
  266.         debug information. Either a package or a pid must be specified.'''
  267.  
  268.         if not self.options.package and not self.options.pid:
  269.             self.ui_error_message(_('No package specified'), 
  270.                 _('You need to specify a package or a PID. See --help for more information.'))
  271.             return False
  272.         self.report = apport.Report('Bug')
  273.         try:
  274.             if self.options.pid:
  275.                 self.report.add_proc_info(self.options.pid)
  276.             else:
  277.                 self.report.add_proc_environ()
  278.         except OSError, e:
  279.             # silently ignore nonexisting PIDs; the user must not close the
  280.             # application prematurely
  281.             if e.errno == errno.ENOENT:
  282.                 return False
  283.             elif e.errno == errno.EACCES:
  284.                 self.ui_error_message(_('Permission denied'), 
  285.                     _('The specified process does not belong to you. Please run this program as the process owner or as root.'))
  286.                 return False
  287.             else:
  288.                 raise
  289.         if self.options.package:
  290.             self.options.package = self.options.package.strip()
  291.         # "Do what I mean" for filing against "linux"
  292.         if self.options.package == 'linux':
  293.             self.cur_package = apport.packaging.get_kernel_package()
  294.         else:
  295.             self.cur_package = self.options.package
  296.  
  297.         try:
  298.             self.collect_info()
  299.         except ValueError, e:
  300.             if str(e) == 'package does not exist':
  301.                 self.ui_error_message(_('Invalid problem report'), 
  302.                     _('Package %s does not exist') % self.cur_package)
  303.                 return False
  304.             else:
  305.                 raise
  306.  
  307.         if not self.handle_duplicate():
  308.             # we do not confirm contents of bug reports, this might have
  309.             # sensitive data
  310.             try:
  311.                 del self.report['ProcCmdline']
  312.             except KeyError:
  313.                 pass
  314.  
  315.             # show what's being sent
  316.             response = self.ui_present_report_details()
  317.             if response != 'cancel':
  318.                 self.file_report()
  319.  
  320.         return True
  321.  
  322.     def run_argv(self):
  323.         '''Call appopriate run_* method according to command line arguments.
  324.         
  325.         Return True if at least one report has been processed, and False
  326.         otherwise.'''
  327.  
  328.         if self.options.filebug:
  329.             return self.run_report_bug()
  330.         elif self.options.crash_file:
  331.             try:
  332.                 self.run_crash(self.options.crash_file, False)
  333.             except OSError, e:
  334.                 self.ui_error_message(_('Invalid problem report'), str(e))
  335.             return True
  336.         else:
  337.             return self.run_crashes()
  338.  
  339.     #
  340.     # functions that implement workflow bits
  341.     #
  342.  
  343.     def parse_argv(self):
  344.         '''Parse command line options and return (options,
  345.         args) tuple.'''
  346.  
  347.         optparser = optparse.OptionParser('%prog [options]')
  348.         optparser.add_option('-f', '--file-bug',
  349.             help='Start in bug filing mode. Requires --package and an optional --pid, or just a --pid',
  350.             action='store_true', dest='filebug', default=False)
  351.         optparser.add_option('-p', '--package',
  352.             help='Specify package name in --file-bug mode. This is optional if a --pid is specified.',
  353.             action='store', type='string', dest='package', default=None)
  354.         optparser.add_option('-P', '--pid',
  355.             help='Specify a running program in --file-bug mode. If this is specified, the bug report will contain more information.',
  356.             action='store', type='int', dest='pid', default=None)
  357.         optparser.add_option('-c', '--crash-file',
  358.             help='Report the crash from given .crash file instead of the pending ones in ' + apport.fileutils.report_dir,
  359.             action='store', type='string', dest='crash_file', default=None, metavar='PATH')
  360.  
  361.         (self.options, self.args) = optparser.parse_args()
  362.  
  363.     def format_filesize(self, size):
  364.         '''Format the given integer as humanly readable and i18n'ed file size.'''
  365.  
  366.         if size < 1048576:
  367.             return locale.format('%.1f KiB', size/1024.)
  368.         if size < 1024 * 1048576:
  369.             return locale.format('%.1f MiB', size / 1048576.)
  370.         return locale.format('%.1f GiB', size / float(1024 * 1048576))
  371.  
  372.     def get_complete_size(self):
  373.         '''Return the size of the complete report.'''
  374.  
  375.         try:
  376.             return self.complete_size
  377.         except AttributeError:
  378.             # report wasn't loaded, so count manually
  379.             size = 0
  380.             for k in self.report:
  381.                 if self.report[k]:
  382.                     size += len(self.report[k])
  383.             return size
  384.  
  385.     def get_reduced_size(self):
  386.         '''Return the size of the reduced report.'''
  387.  
  388.         size = 0
  389.         for k in self.report:
  390.             if k != 'CoreDump':
  391.                 if self.report[k]:
  392.                     size += len(self.report[k])
  393.  
  394.         return size
  395.  
  396.     def restart(self):
  397.         '''Reopen the crashed application.'''
  398.  
  399.         assert self.report.has_key('ProcCmdline')
  400.  
  401.         if os.fork() == 0:
  402.             os.setsid()
  403.             os.execlp('sh', 'sh', '-c', self.report.get('RespawnCommand', self.report['ProcCmdline']))
  404.             sys.exit(1)
  405.  
  406.     def collect_info(self):
  407.         '''Collect missing information about the report from the system and
  408.         display a progress dialog in the meantime.
  409.  
  410.         In particular, this adds OS, package and gdb information and checks bug
  411.         patterns.'''
  412.  
  413.         if not self.cur_package and not self.report.has_key('ExecutablePath'):
  414.             # this happens if we file a bug without specifying a PID or a
  415.             # package
  416.             self.report.add_os_info()
  417.         else:
  418.             # report might already be pre-processed by apport-retrace
  419.             if self.report['ProblemType'] == 'Crash' and 'Stacktrace' in self.report:
  420.                 return
  421.  
  422.             # since this might take a while, create separate threads and
  423.             # display a progress dialog
  424.             self.ui_start_info_collection_progress()
  425.  
  426.             if not self.report.has_key('Stacktrace'):
  427.                 icthread = REThread.REThread(target=thread_collect_info,
  428.                     name='thread_collect_info',
  429.                     args=(self.report, self.report_file, self.cur_package))
  430.                 icthread.start()
  431.                 while icthread.isAlive():
  432.                     self.ui_pulse_info_collection_progress()
  433.                     try:
  434.                         icthread.join(0.1)
  435.                     except KeyboardInterrupt:
  436.                         sys.exit(1)
  437.                 icthread.exc_raise()
  438.  
  439.             if self.report.has_key('CrashDB'):
  440.                 self.crashdb = get_crashdb(None, self.report['CrashDB']) 
  441.  
  442.             if self.report['ProblemType'] == 'KernelCrash' or self.report['ProblemType'] == 'KernelOops' or self.report.has_key('Package'):
  443.                 bpthread = REThread.REThread(target=self.report.search_bug_patterns,
  444.                     args=(self.crashdb.get_bugpattern_baseurl(),))
  445.                 bpthread.start()
  446.                 while bpthread.isAlive():
  447.                     self.ui_pulse_info_collection_progress()
  448.                     try:
  449.                         bpthread.join(0.1)
  450.                     except KeyboardInterrupt:
  451.                         sys.exit(1)
  452.                 bpthread.exc_raise()
  453.                 if bpthread.return_value():
  454.                     self.report['BugPatternURL'] = bpthread.return_value()
  455.  
  456.             self.ui_stop_info_collection_progress()
  457.  
  458.             # check that we were able to determine package names
  459.             if 'SourcePackage' not in self.report or \
  460.                 (not self.report['ProblemType'].startswith('Kernel') and 'Package' not in self.report):
  461.                 self.ui_error_message(_('Invalid problem report'),
  462.                     _('Could not determine the package or source package name.'))
  463.                 # TODO This is not called consistently, is it really needed?
  464.                 self.ui_shutdown()
  465.                 sys.exit(1)
  466.  
  467.     def open_url(self, url):
  468.         '''Open the given URL in a new browser window.
  469.  
  470.         Display an error dialog if everything fails.'''
  471.  
  472.         (r, w) = os.pipe()
  473.         if os.fork() > 0:
  474.             os.close(w)
  475.             (pid, status) = os.wait()
  476.             if status:
  477.                 title = _('Unable to start web browser')
  478.                 error = _('Unable to start web browser to open %s.' % url)
  479.                 message = os.fdopen(r).readline()
  480.                 if message:
  481.                     error += '\n' + message
  482.                 self.ui_error_message(title, error)
  483.             try:
  484.                 os.close(r)
  485.             except OSError:
  486.                 pass
  487.             return
  488.  
  489.         os.setsid()
  490.         os.close(r)
  491.  
  492.         # If we are called through sudo, determine the real user id and run the
  493.         # browser with it to get the user's web browser settings.
  494.         try:
  495.             uid = int(os.getenv('SUDO_UID'))
  496.             gid = int(os.getenv('SUDO_GID'))
  497.             sudo_prefix = ['sudo', '-H', '-u', '#'+str(uid)]
  498.         except (TypeError):
  499.             uid = os.getuid()
  500.             gid = None
  501.             sudo_prefix = []
  502.  
  503.         # figure out appropriate web browser
  504.         try:
  505.             # if ksmserver is running, try kfmclient
  506.             try:
  507.                 if os.getenv('DISPLAY') and \
  508.                         subprocess.call(['pgrep', '-x', '-u', str(uid), 'ksmserver'],
  509.                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
  510.                     subprocess.call(sudo_prefix + ['kfmclient', 'openURL', url])
  511.                     sys.exit(0)
  512.             except OSError:
  513.                 pass
  514.  
  515.             # if gnome-session is running, try gnome-open; special-case firefox
  516.             # (and more generally, mozilla browsers) and epiphany to open a new window
  517.             # with respectively -new-window and --new-window
  518.             try:
  519.                 if os.getenv('DISPLAY') and \
  520.                         subprocess.call(['pgrep', '-x', '-u', str(uid), 'gnome-panel|gconfd-2'],
  521.                                 stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
  522.                     gct = subprocess.Popen(sudo_prefix + ['gconftool', '--get',
  523.                         '/desktop/gnome/url-handlers/http/command'],
  524.                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  525.                     if gct.returncode == 0:
  526.                         preferred_browser = gct.communicate()[0]
  527.                         browser = re.match('((firefox|seamonkey|flock)[^\s]*)', preferred_browser)
  528.                         if browser:
  529.                             subprocess.call(sudo_prefix + [browser.group(0), '-new-window', url])
  530.                             sys.exit(0)
  531.                         browser = re.match('(epiphany[^\s]*)', preferred_browser)
  532.                         if browser:
  533.                             subprocess.call(sudo_prefix + [browser.group(0), '--new-window', url])
  534.                             sys.exit(0)
  535.                     if subprocess.call(sudo_prefix + ['gnome-open', url]) == 0:
  536.                         sys.exit(0)
  537.             except OSError:
  538.                 pass
  539.  
  540.             # fall back to webbrowser
  541.             if uid and gid:
  542.                 os.setgroups([gid])
  543.                 os.setgid(gid)
  544.                 os.setuid(uid)
  545.                 os.unsetenv('SUDO_USER') # to make firefox not croak
  546.                 os.environ['HOME'] = pwd.getpwuid(uid).pw_dir
  547.  
  548.             webbrowser.open(url, new=True, autoraise=True)
  549.             sys.exit(0)
  550.  
  551.         except Exception, e:
  552.             os.write(w, str(e))
  553.             sys.exit(1)
  554.  
  555.     def file_report(self):
  556.         '''Upload the current report to the tracking system and guide the user
  557.         to its web page.'''
  558.  
  559.         # drop PackageArchitecture if equal to Architecture
  560.         if self.report.get('PackageArchitecture') == self.report.get('Architecture'):
  561.             try:
  562.                 del self.report['PackageArchitecture']
  563.             except KeyError:
  564.                 pass
  565.  
  566.         global __upload_progress
  567.         __upload_progress = None
  568.  
  569.         def progress_callback(sent, total):
  570.             global __upload_progress
  571.             __upload_progress = float(sent)/total
  572.  
  573.         self.ui_start_upload_progress()
  574.         upthread = REThread.REThread(target=self.crashdb.upload,
  575.             args=(self.report, progress_callback))
  576.         upthread.start()
  577.         while upthread.isAlive():
  578.             self.ui_set_upload_progress(__upload_progress)
  579.             try:
  580.                 upthread.join(0.1)
  581.             except KeyboardInterrupt:
  582.                 sys.exit(1)
  583.         if upthread.exc_info():
  584.             self.ui_error_message(_('Network problem'),
  585.                 '%s:\n\n%s' % (
  586.                     _('Could not upload report data to crash database'),
  587.                     str(upthread.exc_info()[1])
  588.                 ))
  589.             return
  590.  
  591.         ticket = upthread.return_value()
  592.         self.ui_stop_upload_progress()
  593.  
  594.         url = self.crashdb.get_comment_url(self.report, ticket)
  595.         if url:
  596.             self.open_url(url)
  597.  
  598.     def load_report(self, path):
  599.         '''Load report from given path and do some consistency checks.
  600.  
  601.         This might issue an error message and return False if the report cannot
  602.         be processed, otherwise self.report is initialized and True is
  603.         returned.'''
  604.  
  605.         try:
  606.             self.report = apport.Report()
  607.             self.report.load(open(path), binary='compressed')
  608.         except MemoryError:
  609.             self.report = None
  610.             self.ui_error_message(_('Memory exhaustion'),
  611.                 _('Your system does not have enough memory to process this crash report.'))
  612.             return False
  613.         except IOError, e:
  614.             self.report = None
  615.             self.ui_error_message(_('Invalid problem report'), e.strerror)
  616.             return False
  617.         except (TypeError, ValueError, zlib.error):
  618.             self.report = None
  619.             self.ui_error_message(_('Invalid problem report'),
  620.                 _('This problem report is damaged and cannot be processed.'))
  621.             return False
  622.  
  623.         if self.report.has_key('Package'):
  624.             self.cur_package = self.report['Package'].split()[0]
  625.         else:
  626.             self.cur_package = apport.fileutils.find_file_package(self.report.get('ExecutablePath', ''))
  627.  
  628.         exe_path = self.report.get('InterpreterPath', self.report.get('ExecutablePath'))
  629.         if not self.cur_package and self.report['ProblemType'] != 'KernelCrash' and self.report['ProblemType'] != 'KernelOops' or (
  630.             exe_path and not os.path.exists(exe_path)):
  631.             msg = _('This problem report does not apply to a packaged program.')
  632.             if self.report.has_key('ExecutablePath'):
  633.                 msg = '%s (%s)' % (msg, self.report['ExecutablePath'])
  634.             self.report = None
  635.             self.ui_info_message(_('Invalid problem report'), msg)
  636.             return False
  637.  
  638.         self.complete_size = os.path.getsize(path)
  639.  
  640.         return True
  641.  
  642.     def get_desktop_entry(self):
  643.         '''Try to get a matching .desktop file entry (xdg.DesktopEntry) for the
  644.         current self.report and return it.'''
  645.  
  646.         if self.report.has_key('DesktopFile') and os.path.exists(self.report['DesktopFile']):
  647.             desktop_file = self.report['DesktopFile']
  648.         else:
  649.             desktop_file = apport.fileutils.find_package_desktopfile(self.cur_package)
  650.         if desktop_file:
  651.             try:
  652.                 import xdg.DesktopEntry
  653.                 return xdg.DesktopEntry.DesktopEntry(desktop_file)
  654.             except:
  655.                 return None
  656.  
  657.     def handle_duplicate(self):
  658.         '''Check whether the current bug report is already known as a bug
  659.         pattern, and if so, tell the user about it, open the existing bug, and
  660.         return True.'''
  661.  
  662.         if not self.report.has_key('BugPatternURL'):
  663.             return False
  664.  
  665.         self.ui_info_message(_('Problem already known'),
  666.             _('This problem was already reported in the bug report displayed \
  667. in the web browser. Please check if you can add any further information that \
  668. might be helpful for the developers.'))
  669.  
  670.         self.open_url(self.report['BugPatternURL'])
  671.         return True
  672.  
  673.     #
  674.     # abstract UI methods that must be implemented in derived classes
  675.     #
  676.  
  677.     def ui_present_crash(self, desktopentry):
  678.         '''Inform that a crash has happened for self.report and
  679.         self.cur_package and ask about an action.
  680.  
  681.         If the package can be mapped to a desktop file, an xdg.DesktopEntry is
  682.         passed as an argument; this can be used for enhancing strings, etc.
  683.  
  684.         Return the action and options as a dictionary:
  685.  
  686.         - Valid values for the 'action' key: ignore the crash ('cancel'), restart
  687.           the crashed application ('restart'), or report a bug about the crash
  688.           ('report').
  689.         - Valid values for the 'blacklist' key: True or False (True will cause
  690.           the invocation of report.mark_ignore()).'''
  691.  
  692.         raise NotImplementedError, 'this function must be overridden by subclasses'
  693.  
  694.     def ui_present_package_error(self, desktopentry):
  695.         '''Inform that a package installation/upgrade failure has happened for
  696.         self.report and self.cur_package and ask about an action.
  697.  
  698.         Return the action: ignore ('cancel'), or report a bug about the problem
  699.         ('report').'''
  700.  
  701.         raise NotImplementedError, 'this function must be overridden by subclasses'
  702.  
  703.     def ui_present_kernel_error(self, desktopentry):
  704.         '''Inform that a kernel crash has happened for self.report and
  705.         ask about an action.
  706.  
  707.         Return the action: ignore ('cancel'), or report a bug about the problem
  708.         ('report').'''
  709.  
  710.         raise NotImplementedError, 'this function must be overridden by subclasses'
  711.  
  712.     def ui_present_report_details(self):
  713.         '''Show details of the bug report and choose between sending a complete
  714.         or reduced report.
  715.  
  716.         This function can use the get_complete_size() and get_reduced_size()
  717.         methods to determine the respective size of the data to send, and
  718.         format_filesize() to convert it to a humanly readable form.
  719.  
  720.         Return the action: send full report ('full'), send reduced report
  721.         ('reduced'), or do not send anything ('cancel').'''
  722.  
  723.         raise NotImplementedError, 'this function must be overridden by subclasses'
  724.  
  725.     def ui_info_message(self, title, text):
  726.         '''Show an information message box with given title and text.'''
  727.  
  728.         raise NotImplementedError, 'this function must be overridden by subclasses'
  729.  
  730.     def ui_error_message(self, title, text):
  731.         '''Show an error message box with given title and text.'''
  732.  
  733.         raise NotImplementedError, 'this function must be overridden by subclasses'
  734.  
  735.     def ui_start_info_collection_progress(self):
  736.         '''Open a window with an indefinite progress bar, telling the user to
  737.         wait while debug information is being collected.'''
  738.  
  739.         raise NotImplementedError, 'this function must be overridden by subclasses'
  740.  
  741.     def ui_pulse_info_collection_progress(self):
  742.         '''Advance the progress bar in the debug data collection progress
  743.         window.
  744.  
  745.         This function is called every 100 ms.'''
  746.  
  747.         raise NotImplementedError, 'this function must be overridden by subclasses'
  748.  
  749.     def ui_stop_info_collection_progress(self):
  750.         '''Close debug data collection progress window.'''
  751.  
  752.         raise NotImplementedError, 'this function must be overridden by subclasses'
  753.  
  754.     def ui_start_upload_progress(self):
  755.         '''Open a window with an definite progress bar, telling the user to
  756.         wait while debug information is being uploaded.'''
  757.  
  758.         raise NotImplementedError, 'this function must be overridden by subclasses'
  759.  
  760.     def ui_set_upload_progress(self, progress):
  761.         '''Set the progress bar in the debug data upload progress
  762.         window to the given ratio (between 0 and 1, or None for indefinite
  763.         progress).
  764.  
  765.         This function is called every 100 ms.'''
  766.  
  767.         raise NotImplementedError, 'this function must be overridden by subclasses'
  768.  
  769.     def ui_stop_upload_progress(self):
  770.         '''Close debug data upload progress window.'''
  771.  
  772.         raise NotImplementedError, 'this function must be overridden by subclasses'
  773.  
  774.     def ui_shutdown(self):
  775.         '''This is called right before terminating the program and can be used
  776.         for cleaning up.'''
  777.  
  778.         pass
  779.  
  780. #
  781. # Test suite
  782. #
  783.  
  784. if  __name__ == '__main__':
  785.     import unittest, shutil, signal, tempfile
  786.     from cStringIO import StringIO
  787.     import problem_report
  788.  
  789.     class _TestSuiteUserInterface(UserInterface):
  790.         '''Concrete UserInterface suitable for automatic testing.'''
  791.  
  792.         def __init__(self):
  793.             # use our dummy crashdb
  794.             self.crashdb_conf = tempfile.NamedTemporaryFile()
  795.             print >> self.crashdb_conf, '''default = 'testsuite'
  796. databases = {
  797.     'testsuite': { 
  798.         'impl': 'memory',
  799.         'bug_pattern_base': None
  800.     }
  801. }
  802. '''
  803.             self.crashdb_conf.flush()
  804.  
  805.             os.environ['APPORT_CRASHDB_CONF'] = self.crashdb_conf.name
  806.  
  807.             UserInterface.__init__(self)
  808.  
  809.             # state of progress dialogs
  810.             self.ic_progress_active = False
  811.             self.ic_progress_pulses = 0 # count the pulses
  812.             self.upload_progress_active = False
  813.             self.upload_progress_pulses = 0
  814.  
  815.             # these store the choices the ui_present_* calls do
  816.             self.present_crash_response = None
  817.             self.present_package_error_response = None
  818.             self.present_kernel_error_response = None
  819.             self.present_details_response = None
  820.  
  821.             self.opened_url = None
  822.  
  823.             self.clear_msg()
  824.  
  825.         def clear_msg(self):
  826.             # last message box
  827.             self.msg_title = None
  828.             self.msg_text = None
  829.             self.msg_severity = None # 'warning' or 'error'
  830.  
  831.         def ui_present_crash(self, desktopentry):
  832.             return self.present_crash_response
  833.  
  834.         def ui_present_package_error(self):
  835.             return self.present_package_error_response
  836.  
  837.         def ui_present_kernel_error(self):
  838.             return self.present_kernel_error_response
  839.  
  840.         def ui_present_report_details(self):
  841.             return self.present_details_response
  842.  
  843.         def ui_info_message(self, title, text):
  844.             self.msg_title = title
  845.             self.msg_text = text
  846.             self.msg_severity = 'info'
  847.  
  848.         def ui_error_message(self, title, text):
  849.             self.msg_title = title
  850.             self.msg_text = text
  851.             self.msg_severity = 'error'
  852.  
  853.         def ui_start_info_collection_progress(self):
  854.             self.ic_progress_pulses = 0
  855.             self.ic_progress_active = True
  856.  
  857.         def ui_pulse_info_collection_progress(self):
  858.             assert self.ic_progress_active
  859.             self.ic_progress_pulses += 1
  860.  
  861.         def ui_stop_info_collection_progress(self):
  862.             self.ic_progress_active = False
  863.  
  864.         def ui_start_upload_progress(self):
  865.             self.upload_progress_pulses = 0
  866.             self.upload_progress_active = True
  867.  
  868.         def ui_set_upload_progress(self, progress):
  869.             assert self.upload_progress_active
  870.             self.upload_progress_pulses += 1
  871.  
  872.         def ui_stop_upload_progress(self):
  873.             self.upload_progress_active = False
  874.  
  875.         def open_url(self, url):
  876.             self.opened_url = url
  877.  
  878.     class _UserInterfaceTest(unittest.TestCase):
  879.         def setUp(self):
  880.             # we test a few strings, don't get confused by translations
  881.             for v in ['LANG', 'LANGUAGE', 'LC_MESSAGES', 'LC_ALL']:
  882.                 try:
  883.                     del os.environ[v]
  884.                 except KeyError:
  885.                     pass
  886.  
  887.             self.orig_report_dir = apport.fileutils.report_dir
  888.             apport.fileutils.report_dir = tempfile.mkdtemp()
  889.             self.orig_ignore_file = apport.report._ignore_file
  890.             (fd, apport.report._ignore_file) = tempfile.mkstemp()
  891.             os.close(fd)
  892.  
  893.             # need to do this to not break ui's ctor
  894.             self.orig_argv = sys.argv
  895.             sys.argv = ['ui-test']
  896.             self.ui = _TestSuiteUserInterface()
  897.  
  898.             # demo report
  899.             self.report = apport.Report()
  900.             self.report['Package'] = 'libfoo1 1-1'
  901.             self.report['SourcePackage'] = 'foo'
  902.             self.report['Foo'] = 'A' * 1000
  903.             self.report['CoreDump'] = 'A' * 100000
  904.  
  905.             # write demo report into temporary file
  906.             self.report_file = tempfile.NamedTemporaryFile()
  907.             self.update_report_file()
  908.  
  909.         def update_report_file(self):
  910.             self.report_file.seek(0)
  911.             self.report_file.truncate()
  912.             self.report.write(self.report_file)
  913.             self.report_file.flush()
  914.  
  915.         def tearDown(self):
  916.             sys.argv = self.orig_argv
  917.             shutil.rmtree(apport.fileutils.report_dir)
  918.             apport.fileutils.report_dir = self.orig_report_dir
  919.             self.orig_report_dir = None
  920.  
  921.             os.unlink(apport.report._ignore_file)
  922.             apport.report._ignore_file = self.orig_ignore_file
  923.  
  924.             self.ui = None
  925.             self.report_file.close()
  926.  
  927.             self.assertEqual(subprocess.call(['pidof', '/bin/cat']), 1, 'no stray cats')
  928.             self.assertEqual(subprocess.call(['pidof', '/bin/sleep']), 1, 'no stray sleeps')
  929.  
  930.         def test_format_filesize(self):
  931.             '''format_filesize().'''
  932.  
  933.             self.assertEqual(self.ui.format_filesize(0), '0.0 KiB')
  934.             self.assertEqual(self.ui.format_filesize(2048), '2.0 KiB')
  935.             self.assertEqual(self.ui.format_filesize(2560), '2.5 KiB')
  936.             self.assertEqual(self.ui.format_filesize(1000000), '976.6 KiB')
  937.             self.assertEqual(self.ui.format_filesize(1048576), '1.0 MiB')
  938.             self.assertEqual(self.ui.format_filesize(2.7*1048576), '2.7 MiB')
  939.             self.assertEqual(self.ui.format_filesize(1024*1048576), '1.0 GiB')
  940.             self.assertEqual(self.ui.format_filesize(2560*1048576), '2.5 GiB')
  941.  
  942.         def test_get_size_loaded(self):
  943.             '''get_complete_size() and get_reduced_size() for loaded Reports.'''
  944.  
  945.             self.ui.load_report(self.report_file.name)
  946.  
  947.             self.assertEqual(self.ui.get_complete_size(),
  948.                 os.path.getsize(self.report_file.name))
  949.             rs = self.ui.get_reduced_size()
  950.             self.assert_(rs > 1000)
  951.             self.assert_(rs < 10000)
  952.  
  953.         def test_get_size_constructed(self):
  954.             '''get_complete_size() and get_reduced_size() for on-the-fly Reports.'''
  955.  
  956.             self.ui.report = apport.Report('Bug')
  957.             self.ui.report['Hello'] = 'World'
  958.  
  959.             s = self.ui.get_complete_size()
  960.             self.assert_(s > 5)
  961.             self.assert_(s < 100)
  962.  
  963.             self.assertEqual(s, self.ui.get_reduced_size())
  964.  
  965.         def test_load_report(self):
  966.             '''load_report().'''
  967.  
  968.             # valid report
  969.             self.ui.load_report(self.report_file.name)
  970.             self.assertEqual(self.ui.report, self.report)
  971.             self.assertEqual(self.ui.msg_title, None)
  972.  
  973.             # report without Package
  974.             del self.report['Package']
  975.             del self.report['SourcePackage']
  976.             self.update_report_file()
  977.             self.ui.load_report(self.report_file.name)
  978.  
  979.             self.assert_(self.ui.report == None)
  980.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  981.             self.assertEqual(self.ui.msg_severity, 'info')
  982.  
  983.             self.ui.clear_msg()
  984.  
  985.             # invalid base64 encoding
  986.             self.report_file.seek(0)
  987.             self.report_file.truncate()
  988.             self.report_file.write('''Type: test
  989. Package: foo 1-1
  990. CoreDump: base64
  991.  bOgUs=
  992. ''')
  993.             self.report_file.flush()
  994.  
  995.             self.ui.load_report(self.report_file.name)
  996.             self.assert_(self.ui.report == None)
  997.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  998.             self.assertEqual(self.ui.msg_severity, 'error')
  999.  
  1000.         def test_restart(self):
  1001.             '''restart().'''
  1002.  
  1003.             # test with only ProcCmdline
  1004.             p = os.path.join(apport.fileutils.report_dir, 'ProcCmdline')
  1005.             r = os.path.join(apport.fileutils.report_dir, 'Custom')
  1006.             self.report['ProcCmdline'] = 'touch ' + p
  1007.             self.update_report_file()
  1008.             self.ui.load_report(self.report_file.name)
  1009.  
  1010.             self.ui.restart()
  1011.             time.sleep(1) # FIXME: race condition
  1012.             self.assert_(os.path.exists(p))
  1013.             self.assert_(not os.path.exists(r))
  1014.             os.unlink(p)
  1015.  
  1016.             # test with RespawnCommand
  1017.             self.report['RespawnCommand'] = 'touch ' + r
  1018.             self.update_report_file()
  1019.             self.ui.load_report(self.report_file.name)
  1020.  
  1021.             self.ui.restart()
  1022.             time.sleep(1) # FIXME: race condition
  1023.             self.assert_(not os.path.exists(p))
  1024.             self.assert_(os.path.exists(r))
  1025.             os.unlink(r)
  1026.  
  1027.             # test that invalid command does not make us fall apart
  1028.             del self.report['RespawnCommand']
  1029.             self.report['ProcCmdline'] = '/nonexisting'
  1030.             self.update_report_file()
  1031.             self.ui.load_report(self.report_file.name)
  1032.  
  1033.         def test_collect_info_distro(self):
  1034.             '''collect_info() on report without information (distro bug).'''
  1035.  
  1036.             # report without any information (distro bug)
  1037.             self.ui.report = apport.Report()
  1038.             self.ui.collect_info()
  1039.             self.assert_(set(['Date', 'Uname', 'DistroRelease', 'ProblemType']).issubset(
  1040.                 set(self.ui.report.keys())))
  1041.             self.assertEqual(self.ui.ic_progress_pulses, 0,
  1042.                 'no progress dialog for distro bug info collection')
  1043.  
  1044.         def test_collect_info_exepath(self):
  1045.             '''collect_info() on report with only ExecutablePath.'''
  1046.  
  1047.             # report with only package information
  1048.             self.report = apport.Report()
  1049.             self.report['ExecutablePath'] = '/bin/bash'
  1050.             self.update_report_file()
  1051.             self.ui.load_report(self.report_file.name)
  1052.             # add some tuple values, for robustness testing (might be added by
  1053.             # apport hooks)
  1054.             self.ui.report['Fstab'] = ('/etc/fstab', True)
  1055.             self.ui.report['CompressedValue'] = problem_report.CompressedValue('Test')
  1056.             self.ui.collect_info()
  1057.             self.assert_(set(['SourcePackage', 'Package', 'ProblemType',
  1058.                 'Uname', 'Dependencies', 'DistroRelease', 'Date',
  1059.                 'ExecutablePath']).issubset(set(self.ui.report.keys())))
  1060.             self.assert_(self.ui.ic_progress_pulses > 0,
  1061.                 'progress dialog for package bug info collection')
  1062.             self.assertEqual(self.ui.ic_progress_active, False,
  1063.                 'progress dialog for package bug info collection finished')
  1064.  
  1065.         def test_collect_info_package(self):
  1066.             '''collect_info() on report with a package.'''
  1067.  
  1068.             # report with only package information
  1069.             self.ui.report = apport.Report()
  1070.             self.ui.cur_package = 'bash'
  1071.             self.ui.collect_info()
  1072.             self.assert_(set(['SourcePackage', 'Package', 'ProblemType',
  1073.                 'Uname', 'Dependencies', 'DistroRelease',
  1074.                 'Date']).issubset(set(self.ui.report.keys())))
  1075.             self.assert_(self.ui.ic_progress_pulses > 0,
  1076.                 'progress dialog for package bug info collection')
  1077.             self.assertEqual(self.ui.ic_progress_active, False,
  1078.                 'progress dialog for package bug info collection finished')
  1079.  
  1080.         def test_handle_duplicate(self):
  1081.             '''handle_duplicate().'''
  1082.  
  1083.             self.ui.load_report(self.report_file.name)
  1084.             self.assertEqual(self.ui.handle_duplicate(), False)
  1085.             self.assertEqual(self.ui.msg_title, None)
  1086.             self.assertEqual(self.ui.opened_url, None)
  1087.  
  1088.             demo_url = 'http://example.com/1'
  1089.             self.report['BugPatternURL'] = demo_url
  1090.             self.update_report_file()
  1091.             self.ui.load_report(self.report_file.name)
  1092.             self.assertEqual(self.ui.handle_duplicate(), True)
  1093.             self.assertEqual(self.ui.msg_severity, 'info')
  1094.             self.assertEqual(self.ui.opened_url, demo_url)
  1095.  
  1096.         def test_run_nopending(self):
  1097.             '''running the frontend without any pending reports.'''
  1098.  
  1099.             sys.argv = []
  1100.             self.ui = _TestSuiteUserInterface()
  1101.             self.assertEqual(self.ui.run_argv(), False)
  1102.  
  1103.         def test_run_report_bug_noargs(self):
  1104.             '''run_report_bug() without specifying arguments.'''
  1105.  
  1106.             sys.argv = ['ui-test', '-f']
  1107.             self.ui = _TestSuiteUserInterface()
  1108.             self.assertEqual(self.ui.run_argv(), False)
  1109.             self.assertEqual(self.ui.msg_severity, 'error')
  1110.  
  1111.         def test_run_report_bug_package(self):
  1112.             '''run_report_bug() for a package.'''
  1113.  
  1114.             sys.argv = ['ui-test', '-f', '-p', 'bash']
  1115.             self.ui = _TestSuiteUserInterface()
  1116.             self.assertEqual(self.ui.run_argv(), True)
  1117.  
  1118.             self.assertEqual(self.ui.msg_severity, None)
  1119.             self.assertEqual(self.ui.msg_title, None)
  1120.             self.assertEqual(self.ui.opened_url, 'http://bash.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1121.  
  1122.             self.assert_(self.ui.ic_progress_pulses > 0)
  1123.             self.assertEqual(self.ui.report['SourcePackage'], 'bash')
  1124.             self.assert_('Dependencies' in self.ui.report.keys())
  1125.             self.assert_('ProcEnviron' in self.ui.report.keys())
  1126.             self.assertEqual(self.ui.report['ProblemType'], 'Bug')
  1127.  
  1128.             # should not crash on nonexisting package
  1129.             sys.argv = ['ui-test', '-f', '-p', 'nonexisting_gibberish']
  1130.             self.ui = _TestSuiteUserInterface()
  1131.             self.ui.run_argv()
  1132.  
  1133.             self.assertEqual(self.ui.msg_severity, 'error')
  1134.  
  1135.         def test_run_report_bug_pid(self):
  1136.             '''run_report_bug() for a pid.'''
  1137.  
  1138.             # fork a test process
  1139.             pid = os.fork()
  1140.             if pid == 0:
  1141.                 os.execv('/bin/sleep', ['sleep', '10000'])
  1142.                 assert False, 'Could not execute /bin/sleep'
  1143.  
  1144.             time.sleep(0.5)
  1145.  
  1146.             try:
  1147.                 # report a bug on cat process
  1148.                 sys.argv = ['ui-test', '-f', '-P', str(pid)]
  1149.                 self.ui = _TestSuiteUserInterface()
  1150.                 self.assertEqual(self.ui.run_argv(), True)
  1151.             finally:
  1152.                 # kill test process
  1153.                 os.kill(pid, signal.SIGKILL)
  1154.                 os.waitpid(pid, 0)
  1155.  
  1156.             self.assert_('SourcePackage' in self.ui.report.keys())
  1157.             self.assert_('Dependencies' in self.ui.report.keys())
  1158.             self.assert_('ProcMaps' in self.ui.report.keys())
  1159.             self.assertEqual(self.ui.report['ExecutablePath'], '/bin/sleep')
  1160.             self.failIf(self.ui.report.has_key('ProcCmdline')) # privacy!
  1161.             self.assert_('ProcEnviron' in self.ui.report.keys())
  1162.             self.assertEqual(self.ui.report['ProblemType'], 'Bug')
  1163.  
  1164.             self.assertEqual(self.ui.msg_severity, None)
  1165.             self.assertEqual(self.ui.msg_title, None)
  1166.             self.assertEqual(self.ui.opened_url, 'http://coreutils.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1167.             self.assert_(self.ui.ic_progress_pulses > 0)
  1168.  
  1169.         def test_run_report_bug_wrong_pid(self):
  1170.             '''run_report_bug() for a nonexisting pid.'''
  1171.  
  1172.             # search an unused pid
  1173.             pid = 1
  1174.             while True:
  1175.                 pid += 1
  1176.                 try:
  1177.                     os.kill(pid, 0)
  1178.                 except OSError, e:
  1179.                     if e.errno == errno.ESRCH:
  1180.                         break
  1181.  
  1182.             # silently ignore missing PID; this happens when the user closes
  1183.             # the application prematurely
  1184.             sys.argv = ['ui-test', '-f', '-P', str(pid)]
  1185.             self.ui = _TestSuiteUserInterface()
  1186.             self.ui.run_argv()
  1187.  
  1188.         def test_run_report_bug_noperm_pid(self):
  1189.             '''run_report_bug() for a pid which runs as a different user.'''
  1190.  
  1191.             assert os.getuid() > 0, 'this test must not be run as root'
  1192.  
  1193.             sys.argv = ['ui-test', '-f', '-P', '1']
  1194.             self.ui = _TestSuiteUserInterface()
  1195.             self.ui.run_argv()
  1196.  
  1197.             self.assertEqual(self.ui.msg_severity, 'error')
  1198.  
  1199.         def test_run_report_bug_unpackaged_pid(self):
  1200.             '''run_report_bug() for a pid of an unpackaged program.'''
  1201.  
  1202.             # create unpackaged test program
  1203.             (fd, exename) = tempfile.mkstemp()
  1204.             os.write(fd, open('/bin/cat').read())
  1205.             os.close(fd)
  1206.             os.chmod(exename, 0755)
  1207.  
  1208.             # unpackaged test process
  1209.             pid = os.fork()
  1210.             if pid == 0:
  1211.                 os.execv(exename, [exename])
  1212.  
  1213.             try:
  1214.                 sys.argv = ['ui-test', '-f', '-P', str(pid)]
  1215.                 self.ui = _TestSuiteUserInterface()
  1216.                 self.assertRaises(SystemExit, self.ui.run_argv)
  1217.             finally:
  1218.                 os.kill(pid, signal.SIGKILL)
  1219.                 os.wait()
  1220.                 os.unlink(exename)
  1221.  
  1222.             self.assertEqual(self.ui.msg_severity, 'error')
  1223.  
  1224.         def _gen_test_crash(self):
  1225.             '''Generate a Report with real crash data.'''
  1226.  
  1227.             # create a test executable
  1228.             test_executable = '/bin/cat'
  1229.             assert os.access(test_executable, os.X_OK), test_executable + ' is not executable'
  1230.             pid = os.fork()
  1231.             if pid == 0:
  1232.                 os.setsid()
  1233.                 os.execv(test_executable, [test_executable])
  1234.                 assert False, 'Could not execute ' + test_executable
  1235.  
  1236.             try:
  1237.                 # generate a core dump
  1238.                 time.sleep(0.5)
  1239.                 coredump = os.path.join(apport.fileutils.report_dir, 'core')
  1240.                 assert subprocess.call(['gdb', '--batch', '--ex', 'generate-core-file '
  1241.                     + coredump, test_executable, str(pid)], stdout=subprocess.PIPE,
  1242.                     stderr=subprocess.PIPE) == 0
  1243.  
  1244.                 # generate crash report
  1245.                 r = apport.Report()
  1246.                 r['ExecutablePath'] = test_executable
  1247.                 r['CoreDump'] = (coredump,)
  1248.                 r['Signal'] = '11'
  1249.                 r.add_proc_info(pid)
  1250.                 r.add_user_info()
  1251.             finally:
  1252.                 # kill test executable
  1253.                 os.kill(pid, signal.SIGKILL)
  1254.                 os.waitpid(pid, 0)
  1255.  
  1256.             return r
  1257.  
  1258.         def test_run_crash(self):
  1259.             '''run_crash().'''
  1260.  
  1261.             r = self._gen_test_crash()
  1262.  
  1263.             # write crash report
  1264.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1265.  
  1266.             # cancel crash notification dialog
  1267.             r.write(open(report_file, 'w'))
  1268.             self.ui = _TestSuiteUserInterface()
  1269.             self.ui.present_crash_response = {'action': 'cancel', 'blacklist': False }
  1270.             self.ui.run_crash(report_file)
  1271.             self.assertEqual(self.ui.msg_severity, None)
  1272.             self.assertEqual(self.ui.msg_title, None)
  1273.             self.assertEqual(self.ui.opened_url, None)
  1274.             self.assertEqual(self.ui.ic_progress_pulses, 0)
  1275.  
  1276.             # report in crash notification dialog, cancel details report
  1277.             r.write(open(report_file, 'w'))
  1278.             self.ui = _TestSuiteUserInterface()
  1279.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1280.             self.ui.present_details_response = 'cancel'
  1281.             self.ui.run_crash(report_file)
  1282.             self.assertEqual(self.ui.msg_severity, None, 'has %s message: %s: %s' % (
  1283.                 self.ui.msg_severity, str(self.ui.msg_title), str(self.ui.msg_text)))
  1284.             self.assertEqual(self.ui.msg_title, None)
  1285.             self.assertEqual(self.ui.opened_url, None)
  1286.             self.assertNotEqual(self.ui.ic_progress_pulses, 0)
  1287.  
  1288.             # report in crash notification dialog, send full report
  1289.             r.write(open(report_file, 'w'))
  1290.             self.ui = _TestSuiteUserInterface()
  1291.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1292.             self.ui.present_details_response = 'full'
  1293.             self.ui.run_crash(report_file)
  1294.             self.assertEqual(self.ui.msg_severity, None)
  1295.             self.assertEqual(self.ui.msg_title, None)
  1296.             self.assertEqual(self.ui.opened_url, 'http://coreutils.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1297.             self.assertNotEqual(self.ui.ic_progress_pulses, 0)
  1298.  
  1299.             self.assert_('SourcePackage' in self.ui.report.keys())
  1300.             self.assert_('Dependencies' in self.ui.report.keys())
  1301.             self.assert_('Stacktrace' in self.ui.report.keys())
  1302.             self.assert_('ProcEnviron' in self.ui.report.keys())
  1303.             self.assertEqual(self.ui.report['ProblemType'], 'Crash')
  1304.             self.assert_(len(self.ui.report['CoreDump']) > 10000)
  1305.             self.assert_(self.ui.report['Title'].startswith('cat crashed with SIGSEGV'))
  1306.  
  1307.             # report in crash notification dialog, send reduced report
  1308.             r.write(open(report_file, 'w'))
  1309.             self.ui = _TestSuiteUserInterface()
  1310.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1311.             self.ui.present_details_response = 'reduced'
  1312.             self.ui.run_crash(report_file)
  1313.             self.assertEqual(self.ui.msg_severity, None)
  1314.             self.assertEqual(self.ui.msg_title, None)
  1315.             self.assertEqual(self.ui.opened_url, 'http://coreutils.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1316.             self.assertNotEqual(self.ui.ic_progress_pulses, 0)
  1317.  
  1318.             self.assert_('SourcePackage' in self.ui.report.keys())
  1319.             self.assert_('Dependencies' in self.ui.report.keys())
  1320.             self.assert_('Stacktrace' in self.ui.report.keys())
  1321.             self.assertEqual(self.ui.report['ProblemType'], 'Crash')
  1322.             self.assert_(not self.ui.report.has_key('CoreDump'))
  1323.  
  1324.             # so far we did not blacklist, verify that
  1325.             self.assert_(not self.ui.report.check_ignored())
  1326.  
  1327.             # cancel crash notification dialog and blacklist
  1328.             r.write(open(report_file, 'w'))
  1329.             self.ui = _TestSuiteUserInterface()
  1330.             self.ui.present_crash_response = {'action': 'cancel', 'blacklist': True }
  1331.             self.ui.run_crash(report_file)
  1332.             self.assertEqual(self.ui.msg_severity, None)
  1333.             self.assertEqual(self.ui.msg_title, None)
  1334.             self.assertEqual(self.ui.opened_url, None)
  1335.             self.assertEqual(self.ui.ic_progress_pulses, 0)
  1336.  
  1337.             self.assert_(self.ui.report.check_ignored())
  1338.  
  1339.         def test_run_crash_argv_file(self):
  1340.             '''run_crash() through a file specified on the command line.'''
  1341.  
  1342.             self.report['Package'] = 'bash'
  1343.             self.report['UnsupportableReason'] = 'It stinks.'
  1344.             self.update_report_file()
  1345.  
  1346.             sys.argv = ['ui-test', '-c', self.report_file.name]
  1347.             self.ui = _TestSuiteUserInterface()
  1348.             self.assertEqual(self.ui.run_argv(), True)
  1349.  
  1350.             self.assert_('It stinks.' in self.ui.msg_text, '%s: %s' %
  1351.                 (self.ui.msg_title, self.ui.msg_text))
  1352.             self.assertEqual(self.ui.msg_severity, 'info')
  1353.  
  1354.             # should not die with an exception on an invalid name
  1355.             sys.argv = ['ui-test', '-c', '/nonexisting.crash' ]
  1356.             self.ui = _TestSuiteUserInterface()
  1357.             self.assertEqual(self.ui.run_argv(), True)
  1358.             self.assertEqual(self.ui.msg_severity, 'error')
  1359.  
  1360.         def test_run_crash_unsupportable(self):
  1361.             '''run_crash() on a crash with the UnsupportableReason
  1362.             field.'''
  1363.  
  1364.             self.report['UnsupportableReason'] = 'It stinks.'
  1365.             self.report['Package'] = 'bash'
  1366.             self.update_report_file()
  1367.  
  1368.             self.ui.run_crash(self.report_file.name)
  1369.  
  1370.             self.assert_('It stinks.' in self.ui.msg_text, '%s: %s' %
  1371.                 (self.ui.msg_title, self.ui.msg_text))
  1372.             self.assertEqual(self.ui.msg_severity, 'info')
  1373.  
  1374.         def test_run_crash_unreportable(self):
  1375.             '''run_crash() on a crash with the UnreportableReason
  1376.             field.'''
  1377.  
  1378.             self.report['UnreportableReason'] = 'It stinks.'
  1379.             self.report['ExecutablePath'] = '/bin/bash'
  1380.             self.report['Package'] = 'bash 1'
  1381.             self.update_report_file()
  1382.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1383.             self.ui.present_details_response = 'full'
  1384.  
  1385.             self.ui.run_crash(self.report_file.name)
  1386.  
  1387.             self.assert_('It stinks.' in self.ui.msg_text, '%s: %s' %
  1388.                 (self.ui.msg_title, self.ui.msg_text))
  1389.             self.assertEqual(self.ui.msg_severity, 'info')
  1390.  
  1391.         def test_run_crash_ignore(self):
  1392.             '''run_crash() on a crash with the Ignore field.'''
  1393.  
  1394.             self.report['Ignore'] = 'True'
  1395.             self.report['ExecutablePath'] = '/bin/bash'
  1396.             self.report['Package'] = 'bash 1'
  1397.             self.update_report_file()
  1398.  
  1399.             self.ui.run_crash(self.report_file.name)
  1400.             self.assertEqual(self.ui.msg_severity, None)
  1401.  
  1402.         def test_run_crash_nocore(self):
  1403.             '''run_crash() for a crash dump without CoreDump.'''
  1404.  
  1405.             # create a test executable
  1406.             test_executable = '/bin/cat'
  1407.             assert os.access(test_executable, os.X_OK), test_executable + ' is not executable'
  1408.             pid = os.fork()
  1409.             if pid == 0:
  1410.                 os.setsid()
  1411.                 os.execv(test_executable, [test_executable])
  1412.                 assert False, 'Could not execute ' + test_executable
  1413.  
  1414.             try:
  1415.                 time.sleep(0.5)
  1416.                 # generate crash report
  1417.                 r = apport.Report()
  1418.                 r['ExecutablePath'] = test_executable
  1419.                 r['Signal'] = '42'
  1420.                 r.add_proc_info(pid)
  1421.                 r.add_user_info()
  1422.             finally:
  1423.                 # kill test executable
  1424.                 os.kill(pid, signal.SIGKILL)
  1425.                 os.waitpid(pid, 0)
  1426.  
  1427.             # write crash report
  1428.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1429.             r.write(open(report_file, 'w'))
  1430.  
  1431.             # run
  1432.             self.ui = _TestSuiteUserInterface()
  1433.             self.ui.run_crash(report_file)
  1434.             self.assertEqual(self.ui.msg_severity, 'error')
  1435.             self.assert_('memory' in self.ui.msg_text, '%s: %s' %
  1436.                 (self.ui.msg_title, self.ui.msg_text))
  1437.  
  1438.         def test_run_crash_preretraced(self):
  1439.             '''run_crash() pre-retraced reports.
  1440.             
  1441.             This happens with crashes which are pre-processed by
  1442.             apport-retrace.'''
  1443.  
  1444.             r = self._gen_test_crash()
  1445.  
  1446.             #  effect of apport-retrace -c
  1447.             r.add_gdb_info()
  1448.             del r['CoreDump']
  1449.  
  1450.             # write crash report
  1451.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1452.  
  1453.             # report in crash notification dialog, cancel details report
  1454.             r.write(open(report_file, 'w'))
  1455.             self.ui = _TestSuiteUserInterface()
  1456.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1457.             self.ui.present_details_response = 'cancel'
  1458.             self.ui.run_crash(report_file)
  1459.             self.assertEqual(self.ui.msg_severity, None, 'has %s message: %s: %s' % (
  1460.                 self.ui.msg_severity, str(self.ui.msg_title), str(self.ui.msg_text)))
  1461.             self.assertEqual(self.ui.msg_title, None)
  1462.             self.assertEqual(self.ui.opened_url, None)
  1463.             self.assertEqual(self.ui.ic_progress_pulses, 0)
  1464.            
  1465.         def test_run_crash_errors(self):
  1466.             '''run_crash() on various error conditions.'''
  1467.  
  1468.             # crash report with invalid Package name
  1469.             r = apport.Report()
  1470.             r['ExecutablePath'] = '/bin/bash'
  1471.             r['Package'] = 'foobarbaz'
  1472.             r['SourcePackage'] = 'foobarbaz'
  1473.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1474.             r.write(open(report_file, 'w'))
  1475.  
  1476.             self.ui.run_crash(report_file)
  1477.  
  1478.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  1479.             self.assertEqual(self.ui.msg_severity, 'error')
  1480.  
  1481.         def test_run_crash_uninstalled(self):
  1482.             '''run_crash() on reports with subsequently uninstalled packages'''
  1483.  
  1484.             # program got uninstalled between crash and report
  1485.             r = self._gen_test_crash()
  1486.             r['ExecutablePath'] = '/bin/nonexisting'
  1487.             r['Package'] = 'bash'
  1488.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1489.             r.write(open(report_file, 'w'))
  1490.  
  1491.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1492.             self.ui.run_crash(report_file)
  1493.  
  1494.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  1495.             self.assertEqual(self.ui.msg_severity, 'info')
  1496.  
  1497.             # interpreted program got uninstalled between crash and report
  1498.             r = apport.Report()
  1499.             r['ExecutablePath'] = '/bin/nonexisting'
  1500.             r['InterpreterPath'] = '/usr/bin/python'
  1501.             r['Traceback'] = 'ZeroDivisionError: integer division or modulo by zero'
  1502.  
  1503.             self.ui.run_crash(report_file)
  1504.  
  1505.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  1506.             self.assertEqual(self.ui.msg_severity, 'info')
  1507.  
  1508.             # interpreter got uninstalled between crash and report
  1509.             r = apport.Report()
  1510.             r['ExecutablePath'] = '/bin/sh'
  1511.             r['InterpreterPath'] = '/usr/bin/nonexisting'
  1512.             r['Traceback'] = 'ZeroDivisionError: integer division or modulo by zero'
  1513.  
  1514.             self.ui.run_crash(report_file)
  1515.  
  1516.             self.assertEqual(self.ui.msg_title, _('Invalid problem report'))
  1517.             self.assertEqual(self.ui.msg_severity, 'info')
  1518.  
  1519.         def test_run_crash_package(self):
  1520.             '''run_crash() for a package error.'''
  1521.  
  1522.             # generate crash report
  1523.             r = apport.Report('Package')
  1524.             r['Package'] = 'bash'
  1525.             r['SourcePackage'] = 'bash'
  1526.             r['ErrorMessage'] = 'It broke'
  1527.             r['VarLogPackagerlog'] = 'foo\nbar'
  1528.             r.add_os_info()
  1529.  
  1530.             # write crash report
  1531.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1532.  
  1533.             # cancel crash notification dialog
  1534.             r.write(open(report_file, 'w'))
  1535.             self.ui = _TestSuiteUserInterface()
  1536.             self.ui.present_package_error_response = 'cancel'
  1537.             self.ui.run_crash(report_file)
  1538.             self.assertEqual(self.ui.msg_severity, None)
  1539.             self.assertEqual(self.ui.msg_title, None)
  1540.             self.assertEqual(self.ui.opened_url, None)
  1541.             self.assertEqual(self.ui.ic_progress_pulses, 0)
  1542.  
  1543.             # report in crash notification dialog, send report
  1544.             r.write(open(report_file, 'w'))
  1545.             self.ui = _TestSuiteUserInterface()
  1546.             self.ui.present_package_error_response = 'report'
  1547.             self.ui.run_crash(report_file)
  1548.             self.assertEqual(self.ui.msg_severity, None)
  1549.             self.assertEqual(self.ui.msg_title, None)
  1550.             self.assertEqual(self.ui.opened_url, 'http://bash.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1551.  
  1552.             self.assert_('SourcePackage' in self.ui.report.keys())
  1553.             self.assert_('Package' in self.ui.report.keys())
  1554.             self.assertEqual(self.ui.report['ProblemType'], 'Package')
  1555.  
  1556.             # verify that additional information has been collected
  1557.             self.assert_('Architecture' in self.ui.report.keys())
  1558.             self.assert_('DistroRelease' in self.ui.report.keys())
  1559.             self.assert_('Uname' in self.ui.report.keys())
  1560.  
  1561.         def test_run_crash_kernel(self):
  1562.             '''run_crash() for a kernel error.'''
  1563.  
  1564.             # generate crash report
  1565.             r = apport.Report('KernelCrash')
  1566.             r['Package'] = apport.packaging.get_kernel_package()
  1567.             r['SourcePackage'] = 'linux'
  1568.  
  1569.             # write crash report
  1570.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1571.  
  1572.             # cancel crash notification dialog
  1573.             r.write(open(report_file, 'w'))
  1574.             self.ui = _TestSuiteUserInterface()
  1575.             self.ui.present_kernel_error_response = 'cancel'
  1576.             self.ui.run_crash(report_file)
  1577.             self.assertEqual(self.ui.msg_severity, None, 'error: %s - %s' %
  1578.                 (self.ui.msg_title, self.ui.msg_text))
  1579.             self.assertEqual(self.ui.msg_title, None)
  1580.             self.assertEqual(self.ui.opened_url, None)
  1581.             self.assertEqual(self.ui.ic_progress_pulses, 0)
  1582.  
  1583.             # report in crash notification dialog, send report
  1584.             r.write(open(report_file, 'w'))
  1585.             self.ui = _TestSuiteUserInterface()
  1586.             self.ui.present_kernel_error_response = 'report'
  1587.             self.ui.present_details_response = 'full'
  1588.             self.ui.run_crash(report_file)
  1589.             self.assertEqual(self.ui.msg_severity, None, str(self.ui.msg_title) + 
  1590.                 ' ' + str(self.ui.msg_text))
  1591.             self.assertEqual(self.ui.msg_title, None)
  1592.             self.assertEqual(self.ui.opened_url, 'http://linux.bugs.example.com/%i' % self.ui.crashdb.latest_id())
  1593.  
  1594.             self.assert_('SourcePackage' in self.ui.report.keys())
  1595.             # did we run the hooks properly?
  1596.             self.assert_('ProcModules' in self.ui.report.keys())
  1597.             self.assert_('Lspci' in self.ui.report.keys())
  1598.             self.assertEqual(self.ui.report['ProblemType'], 'KernelCrash')
  1599.  
  1600.         def test_run_crash_anonymity(self):
  1601.             '''run_crash() anonymization.'''
  1602.  
  1603.             r = self._gen_test_crash()
  1604.             report_file = os.path.join(apport.fileutils.report_dir, 'test.crash')
  1605.             r.write(open(report_file, 'w'))
  1606.             self.ui = _TestSuiteUserInterface()
  1607.             self.ui.present_crash_response = {'action': 'report', 'blacklist': False }
  1608.             self.ui.present_details_response = 'cancel'
  1609.             self.ui.run_crash(report_file)
  1610.  
  1611.             self.failIf('ProcCwd' in self.ui.report)
  1612.  
  1613.             dump = StringIO()
  1614.             self.ui.report.write(dump)
  1615.  
  1616.             p = pwd.getpwuid(os.getuid())
  1617.             bad_strings = [os.uname()[1], p[0], p[4], p[5], os.getcwd()]
  1618.  
  1619.             for s in bad_strings:
  1620.                 self.failIf(s in dump.getvalue(), 'dump contains sensitive string: %s' % s)
  1621.  
  1622.  
  1623.     unittest.main()
  1624.  
  1625.